guest-agent: freeze the v0 API at v0.5.11 and add the versioned v1 API - #1116
Merged
Conversation
0.6.0 dropped the `Verify` RPC because verification needs no key material and no attestation, and the agent's verdict arrives over the socket unattested -- the reasoning still holds, and SDKs >= 0.6 verify locally. But dropping it broke every 0.5.x SDK against a 0.6 agent for a method those SDKs still call, and prpc answers an unknown method with a bare HTTP 400, so the caller cannot tell a removed method from a wrong socket. The unversioned surface is what 0.5.x clients speak; new API design belongs in `dstack.guest.v1`, not in breaking this one. Restore the RPC and its messages at the v0.5.11 field numbers, marked deprecated and legacy-only, and annotate the service as frozen: no renumbering, no removals, no semantic changes. Semantics are identical to 0.5.11, including k256 rejecting non-canonical high-S signatures outright.
`EmitEvent` is gone for good -- runtime RTMR3 events are system-owned in 0.6.0, so an app extending the measurement chain is no longer something this API should offer. Deleting the method outright is the wrong way to say that. prpc answers both "no such method" and "the handler failed" with HTTP 400 and drops the message, so a 0.5.x app that emits events gets a bare 400 that reads identically to a wrong socket path or a broken build, and its author has no way to learn why the events stopped being recorded. Keep the method and the args message on the unversioned service, and have the handler always bail with the reason. Nothing reaches `emit_runtime_event`; this is a deliberate error, not a code path.
kvinwang
marked this pull request as ready for review
August 24, 2026 13:51
…ferences Six comments still said prpc answers an unknown method with HTTP 400 and drops the message. On this branch an unknown method on a mounted surface is a 404 whose body names it, a handler failure is a 400 with the message in the body, and an absent mount is the server's own 404 page -- the spec's status-code table and the server.rs probe test both say so. Each comment keeps its design point (the EmitEvent stub's self-explanatory error, the in-band Health verdict, declare-not-probe registration) and loses the wrong rationale: the gateway treats every failed poll alike, which is the real reason a "cannot see the app" verdict must arrive as data. Two comments on the frozen GetAttestationForAppKey pointed at `WorkerV1.AttestAppKey`, a method an earlier design had and this branch deliberately does not ship. They now describe the actual v1 flow: derive at /v1/GetKey, commit the public key into report_data, attest via /v1/Attest. cvm-boundaries.md carried the same leftover on its external-listener table. Comment-only in both protos; the frozen-surface digest is unmoved.
…path `AppState::identity()` caches a success forever, but a failure was retried on every call -- and the retry is a hardware quote plus an RTMR replay under the global quote lock, reachable from the anonymous, publicly served `/prpc/v1/Info`. So on a platform whose boot-time decode failed, any caller that could route to the CVM could drive the quote path per request, which is exactly the lever the identity cache exists to remove. A failed decode now arms a 30-second floor: within the window `identity()` reports the identity unavailable without touching the platform, and the startup attempt counts against the same budget. The decode stays on the executor -- at one attempt per interval for the whole process, a `spawn_blocking` hop and an async `identity()` would cost more than they buy. Success semantics are unchanged: cached once, shared by pointer. The fixture platform gains a probe counting `attestation_for_info` calls, which is the only way to see the throttle at all -- what it changes is how often the platform is touched, not what any single call returns. One test pins that a failure is not retried inside the window, one that the retry happens after it and a recovery is cached like any other success.
This was referenced Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Single PR for the dstack 0.6.0 guest-agent API: freezes the unversioned surface at exactly v0.5.11, introduces the versioned v1 API, and switches the four SDKs to mirror both surfaces. Supersedes #1113/#1114.
Problem
nexthad broken the unversioned paths that 0.5.x clients call (Verify/EmitEventdeleted,Worker.GetAttestationForAppKeyrenamed), while the unversioned API carried design debt it could not shed without further breakage:GetTlsKeynames a key what is really certificate issuance;GetKey'spurpose/algorithmare accepted but do not domain-separate the derived key (one 32-byte secret serves both curves);Sign/Verifyare pure computation granting no capabilityGetKeydoes not already grant;Inforeturns triple-nested JSON duplicating its own top-level fields; and the chain claimkeccak256("{purpose}:{hex(pubkey)}")lets a caller-chosenpurposemint nearly arbitrary signed strings.Fix
Every surface becomes a frozen v0 fossil plus a v1, selected by URL path only (no header negotiation):
/(alias),/v0: 9 v0.5.11 methods/v1: IssueCert, GetKey, Attest, AttestGpu, Info, Version/prpc(alias),/prpc/v0: Info, Version, GetAttestationForAppKey/prpc/v1: Info, Version, Healthv0:
Verifyrestored byte-identical to v0.5.11;EmitEventrestored as a deliberate always-failing stub (RTMR3 runtime events are system-owned; the message is the signal, since prpc reports handler failure and unknown method differently only in the body);GetAttestationForAppKeyrestored TDX-only. Never-released post-0.5.11 additions removed. A descriptor-digest test pins the frozen shape so even a wire-compatible field addition fails CI.v1 keeps only what needs the TEE — derivation, attestation, and KMS-backed cert issuance:
GetKey(domain, algorithm):HKDF-SHA256(salt="dstack-guest-v1", IKM=app_root_k256, info=LP("dstack-guest-v1-key")||LP(algorithm)||LP(domain))— length-prefixed encoding, per-algorithm keys, v1-specific salt so no v0 input can reproduce a v1 key. Chain claim binds(algorithm, domain, raw pubkey)and is structurally unforgeable from the v0 claim format (a v0 preimage's tail is lowercase hex; the v1 length prefix contains 0x00). Constants and vectors live inra_tls::api_v1, shared by agent and tests.IssueCert: honest name for whatGetTlsKeydid — certificate issuance via KMSSignCert, generating a fresh key as the integrated one-step mode.Attest(sole CVM attestation entry; optional boot-time GPU evidence),AttestGpu(on-demand, 32-byte nonce), flat identity-and-configurationInfo(attestation data deliberately excluded — it belongs toAttest, quote-backed),Version./prpc/v1/Health. Identity forInfois decoded once at startup (an anonymous external caller can no longer drive hardware quote generation per request).SDKs (all four):
ClientV0mirrors the frozen surface exactly (v0.5.11 method set,verify()back,emit_event()surfaces the removal error);ClientV1mirrors the six v1 methods; the never-released localverify_signature/verify_signature_chainhelpers and their vectors are removed — the normative spec is the verification reference. Deprecated aliases keep existing code compiling. Versions unified at 0.6.0.Spec:
docs/guest-api-v1.md— byte-level KDF/claim encodings, verification steps, status-code/probe rules (measured: absent mount → 404 HTML, unknown method → 404 JSON, failed handler → 400 JSON), migration contract including that v1 keys deliberately differ from v0 keys for the same inputs.Verification
v0.5.11tag (comments stripped): frozenDstackGuest,Worker,Tappdidentical; 16/19 messages byte-identical, 3 differ byreservedonly../dstack/run-tests.sh(agent-backed simulator): 1235 passed, 0 failed.sdk/run-tests.sh: all four suites green (Rust 38, Go 51, Python 156, JS 131).a_v0_path_cannot_reproduce_a_v1_key; forgery test asserts the structural hex/0x00 property; frozen-descriptor test verified to catch a planted field addition; route tests prove/v0and unversioned are the same handler and each mount serves only its own surface.Known follow-ups (out of scope)
asUint8Array→toPkcs8Der, Gousage_server_authdefault, JS transport status checks).sdk/simulator/attestation.binwas captured from a different app-compose thanapp-compose.json(compose_hashmismatch, reproducible on v0 too) — needs a fixture refresh.